// ==UserScript== // @name Via Adblock 规则分析 // @namespace https://viayoo.com/ // @version 1.18 // @description 解析Adblock规则,是否值得在Via浏览器上订阅,评分仅供娱乐,自行斟酌。 // @author Grok & Via // @match *://* // @license MIT // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @run-at document-start // ==/UserScript== (function() { 'use strict'; console.log('Adblock Rule Analyzer 脚本已加载,URL:', location.href); // 使用 GM_getValue 存储自动识别开关,默认关闭 let autoDetectRawText = GM_getValue('autoDetectRawText', false); // 注册菜单项 GM_registerMenuCommand("分析当前页面规则", analyzeCurrentPage); GM_registerMenuCommand("分析自定义链接规则", analyzeCustomLink); GM_registerMenuCommand(`自动识别纯文本链接解析 (${autoDetectRawText ? '开启' : '关闭'})`, toggleAutoDetect); // 简洁的 toast 调用函数 const toast = msg => window.via?.toast?.(msg); // 检查是否是纯文本页面并直接处理 function handleRawTextPage() { if (!autoDetectRawText) return false; const url = location.href; if (url.match(/\.(txt|list|rules|prop)$/i) || url.includes('raw.githubusercontent.com')) { console.log('检测到纯文本页面:', url); toast('正在分析Adblock规则中……') fetchContent(url); return true; } return false; } // 切换自动识别开关 function toggleAutoDetect() { autoDetectRawText = !autoDetectRawText; GM_setValue('autoDetectRawText', autoDetectRawText); toast(`自动识别纯文本链接解析已${autoDetectRawText ? '开启' : '关闭'},刷新页面后生效`); // 更新菜单显示 GM_registerMenuCommand(`自动识别纯文本链接解析 (${autoDetectRawText ? '开启' : '关闭'})`, toggleAutoDetect); } // 在脚本启动时检查是否需要自动处理 if (handleRawTextPage()) { return; } // 通用 fetch 函数 async function fetchContent(url) { try { const response = await fetch(url, { method: 'GET', credentials: 'omit', cache: 'no-store' }); if (!response.ok) { throw new Error(`网络请求失败,状态码: ${response.status} (${response.statusText})`); } const contentType = response.headers.get('Content-Type') || ''; if (!contentType.includes('text/')) { throw new Error('非文本内容,无法解析 (Content-Type: ' + contentType + ')'); } const content = await response.text(); console.log('内容获取成功,长度:', content.length); analyzeContent(content, url); } catch (e) { console.error('内容获取失败:', e); let errorMsg = '无法获取内容:'; if (e.message.includes('Failed to fetch')) { errorMsg += '网络请求失败,可能是链接不可访问或被浏览器阻止(检查 CORS 或网络连接)。'; } else { errorMsg += e.message; } errorMsg += '\n请确保链接有效且指向 Adblock 规则文件。'; alert(errorMsg); } } async function analyzeCurrentPage() { toast('分析当前页面'); fetchContent(location.href); } function analyzeCustomLink() { console.log('分析自定义链接'); const url = prompt('请输入Adblock规则文件的直链(如 https://raw.githubusercontent.com/...)'); if (!url || !url.trim()) { alert('未输入有效的链接'); return; } if (!url.match(/^https?:\/\/.+/)) { alert('链接格式无效,请输入以 http:// 或 https:// 开头的完整 URL'); return; } toast(`解析链接中……`); fetchContent(url); } function normalizeNewlines(text) { return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); } function parseHeader(content) { const header = { title: '未知标题', description: '未添加任何描述', version: '未知版本', lastModified: '未知时间', expires: '未给出更新周期', }; const headerLines = content.split('\n') .filter(line => line.trim().startsWith('!')) .map(line => line.trim().substring(1).trim()); headerLines.forEach(line => { if (line.startsWith('Title:')) header.title = line.substring(6).trim(); else if (line.startsWith('Description:')) header.description = line.substring(12).trim(); else if (line.startsWith('Version:')) header.version = line.substring(8).trim(); else if (line.startsWith('TimeUpdated:') || line.startsWith('Last modified:') || line.startsWith('Update Time:')) { header.lastModified = line.split(':').slice(1).join(':').trim(); } else if (line.startsWith('Expires:')) header.expires = line.substring(8).trim(); }); return header; } function analyzeContent(content, source) { if (!content.startsWith('[Adblock') && !content.startsWith('![Adblock')) { toast(`这不是一个标准的Adblock规则文件(未找到[Adblock开头),来源: ${source}`); console.log('非Adblock文件,来源:', source); return; } content = normalizeNewlines(content); const header = parseHeader(content); const lines = content.split('\n') .filter(line => line.trim() && !line.trim().startsWith('!') && !line.trim().startsWith('[')); const stats = { cssRules: { normal: 0, exception: 0, hasNotPseudo: 0, hasSpecialPseudo: 0, hasSpecialPseudoNotAfter: 0 }, domainRules: { count: 0, duplicateRules: 0 }, unsupported: 0, extendedRules: { scriptInject: 0, adguardScript: 0, htmlFilter: 0, cssInject: 0, other: 0 } }; const extendedPatterns = { scriptInject: /(##|@#+)\+js\(/, adguardScript: /#@?%#/, htmlFilter: /\$\$/, cssInject: /#@?\$#/, specialPseudo: /:matches-property\b|:style\b|:-abp-properties\b|:-abp-contains\b|:min-text-length\b|:matches-path\b|:contains\b|:has-text\b|:matches-css\b|:matches-css-before\b|:matches-css-after\b|:if\b|:if-not\b|:xpath\b|:nth-ancestor\b|:upward\b|:remove\b/, other: /\$(\s*)(redirect|rewrite|csp|removeparam|badfilter|empty|generichide|match-case|object|object-subrequest|important|popup|document)|,(\s*)(redirect=|app=|replace=|csp=|denyallow=|permissions=)|:matches-property\b|:style\b|:-abp-properties\b|:-abp-contains\b|:min-text-length\b|:matches-path\b|:contains\b|:has-text\b|:matches-css\b|:matches-css-before\b|:matches-css-after\b|:if\b|:if-not\b|:xpath\b|:nth-ancestor\b|:upward\b|:remove\b|redirect-rule/ }; const rulePatternMap = new Map(); lines.forEach(line => { const trimmed = line.trim(); if (extendedPatterns.scriptInject.test(trimmed)) { stats.extendedRules.scriptInject++; stats.unsupported++; } else if (extendedPatterns.adguardScript.test(trimmed)) { stats.extendedRules.adguardScript++; stats.unsupported++; } else if (extendedPatterns.htmlFilter.test(trimmed)) { stats.extendedRules.htmlFilter++; stats.unsupported++; } else if (extendedPatterns.cssInject.test(trimmed)) { stats.extendedRules.cssInject++; stats.unsupported++; } else if (extendedPatterns.other.test(trimmed)) { stats.extendedRules.other++; stats.unsupported++; } else if (trimmed.startsWith('##') || trimmed.startsWith('###')) { stats.cssRules.normal++; if (/:has|:not/.test(trimmed)) stats.cssRules.hasNotPseudo++; if (extendedPatterns.specialPseudo.test(trimmed)) stats.cssRules.hasSpecialPseudo++; } else if (trimmed.startsWith('#@#') || trimmed.startsWith('#@##')) { stats.cssRules.exception++; if (/:has|:not/.test(trimmed)) stats.cssRules.hasNotPseudo++; if (extendedPatterns.specialPseudo.test(trimmed)) stats.cssRules.hasSpecialPseudo++; } else if (trimmed.startsWith('||')) { stats.domainRules.count++; let rulePattern = trimmed; let domains = []; const domainMatch = trimmed.match(/[,|$]domain=([^$|,]+)/); if (domainMatch) { rulePattern = trimmed.replace(/[,|$]domain=[^$|,]+/, '').replace(/[,|$].*$/, ''); domains = domainMatch[1].split('|'); } if (rulePatternMap.has(rulePattern)) { const ruleData = rulePatternMap.get(rulePattern); ruleData.count++; stats.domainRules.duplicateRules++; domains.forEach(domain => ruleData.domains.add(domain)); } else { rulePatternMap.set(rulePattern, { domains: new Set(domains), count: 1 }); } } // 检测不在合法位置的特殊伪类 if (extendedPatterns.specialPseudo.test(trimmed)) { if (!trimmed.match(/^(##|###|#@#|#@##|#?#|\$\$)/)) { stats.cssRules.hasSpecialPseudoNotAfter++; } } }); const totalCssRules = stats.cssRules.normal + stats.cssRules.exception; const totalExtendedRules = stats.extendedRules.scriptInject + stats.extendedRules.adguardScript + stats.extendedRules.htmlFilter + stats.extendedRules.cssInject + stats.extendedRules.other; let score = 0; let cssCountScore = Math.max(0, totalCssRules <= 5000 ? 35 : totalCssRules <= 7000 ? 35 - ((totalCssRules - 5000) / 2000) * 10 : totalCssRules <= 9999 ? 25 - ((totalCssRules - 7000) / 2999) * 15 : 10 - ((totalCssRules - 9999) / 5000) * 10); score += cssCountScore; let cssPseudoScore = stats.cssRules.hasNotPseudo <= 30 ? 15 : stats.cssRules.hasNotPseudo <= 100 ? mě10 : stats.cssRules.hasNotPseudo <= 120 ? 5 : 0; score += cssPseudoScore; let domainCountScore = Math.max(0, stats.domainRules.count <= 100000 ? 30 : stats.domainRules.count <= 200000 ? 30 - ((stats.domainRules.count - 100000) / 100000) * 10 : stats.domainRules.count <= 500000 ? 20 - ((stats.domainRules.count - 200000) / 300000) * 15 : 5 - ((stats.domainRules.count - 500000) / 500000) * 5); score += domainCountScore; let domainDuplicateScore = Math.max(0, stats.domainRules.duplicateRules <= 100 ? 10 : stats.domainRules.duplicateRules <= 300 ? 10 - ((stats.domainRules.duplicateRules - 50) / 150) * 5 : 5 - ((stats.domainRules.duplicateRules - 200) / 200) * 5); score += domainDuplicateScore; let extendedScore = totalExtendedRules === 0 ? 10 : totalExtendedRules <= 100 ? 10 - (totalExtendedRules / 100) * 5 : totalExtendedRules <= 300 ? 5 - ((totalExtendedRules - 100) / 200) * 5 : Math.max(-10, 0 - ((totalExtendedRules - 300) / 300) * 10); score += extendedScore; let specialPseudoPenalty = stats.cssRules.hasSpecialPseudo > 0 ? -40 : 0; score += specialPseudoPenalty; let specialPseudoNotAfterPenalty = stats.cssRules.hasSpecialPseudoNotAfter > 0 ? -10 : 0; score += specialPseudoNotAfterPenalty; score = Math.max(1, Math.min(100, Math.round(score))); const cssPerformance = totalCssRules <= 5000 ? '✅CSS规则数量正常,可以流畅运行' : totalCssRules <= 7000 ? '❓CSS规则数量较多,可能会导致设备运行缓慢' : totalCssRules < 9999 ? '⚠️CSS规则数量接近上限,可能明显影响设备性能' : '🆘CSS规则数量过多,不建议订阅此规则'; const domainPerformance = stats.domainRules.count <= 100000 ? '✅域名规则数量正常,可以流畅运行' : stats.domainRules.count <= 200000 ? '❓域名规则数量较多,但仍在可接受范围内' : stats.domainRules.count <= 500000 ? '🆘域名规则数量过多,可能会导致内存溢出 (OOM)' : '‼️域名规则数量极多,强烈不建议使用,可能严重影响性能'; const report = ` Adblock规则分析结果(来源: ${source}): 📜Adblock规则信息: 标题: ${header.title} 描述: ${header.description} 版本: ${header.version} 最后更新: ${header.lastModified} 更新周期: ${header.expires} --------------------- 💯规则评级: ${score}/100 (评分仅供参考,具体以Via变动为主) 📊各部分得分: CSS数量得分: ${Math.round(cssCountScore)}/35 CSS伪类得分: ${cssPseudoScore}/15 域名数量得分: ${Math.round(domainCountScore)}/30 重复规则得分: ${Math.round(domainDuplicateScore)}/10 扩展规则加减分: ${Math.round(extendedScore)} (±10) 特殊伪类惩罚: ${specialPseudoPenalty} (Adguard/uBlock特殊伪类) 特殊伪类不按语法: ${specialPseudoNotAfterPenalty} (未使用正确语法) --------------------- 🛠️总规则数: ${lines.length} 👋不支持的规则: ${stats.unsupported} 📋CSS通用隐藏规则: 常规规则 (##, ###): ${stats.cssRules.normal} 例外规则 (#@#, #@##): ${stats.cssRules.exception} 含:has/:not伪类规则: ${stats.cssRules.hasNotPseudo} 含Adguard/uBlock特殊伪类: ${stats.cssRules.hasSpecialPseudo} 特殊伪类未使用正确语法: ${stats.cssRules.hasSpecialPseudoNotAfter} 总CSS规则数: ${totalCssRules} 性能评估: ${cssPerformance} 🔗域名规则 (||): 总数: ${stats.domainRules.count} 重复规则数: ${stats.domainRules.duplicateRules} 性能评估: ${domainPerformance} ✋🏼uBlock/AdGuard 独有规则: 脚本注入 (##+js): ${stats.extendedRules.scriptInject} AdGuard脚本 (#%#): ${stats.extendedRules.adguardScript} HTML过滤 ($$): ${stats.extendedRules.htmlFilter} CSS注入 (#$#): ${stats.extendedRules.cssInject} 其他扩展规则 ($redirect等): ${stats.extendedRules.other} 总计: ${totalExtendedRules} 注:uBlock/AdGuard 独有规则及特殊伪类在传统 Adblock Plus 中不受支持 `; alert(report); console.log(report); } })();